/*
 * Copyright (c) 2007 jMouse Gestures
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMouse Gestures' nor the names of its contributors 
 *   may be used to endorse or promote products derived from this software 
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.jmg.gesture;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import com.jmg.gesture.event.GestureListener;
import com.jmg.mouse.MouseMovement;
import com.jmg.space.CoordinateFrame;
import com.jmg.space.Location;


/**
 * GestureManager class is an abstract class that defines several methods
 * for managing groups of 
 * {@link com.jmg.gesture.GestureGroup GestureGroup} classes.
 * <p>
 * The generic parameter C is used to allow subclasses to define the type
 * of class this manager can be registered with to monitor for gestures.
 * </p><p>
 * See {@link com.jmg.swing.SwingGestureManager SwingGestureManager}
 * for a concrete example of this class.
 * </p>
 * 
 * @param   <C> The generic component that the manager can be registered on.
 * @author  Christopher Martin
 * @version $Id: GestureManager.java 13 2007-05-24 12:13:13Z GentlemanHal $
 */
public abstract class GestureManager<C> implements 
    Serializable
{
    /** Serial Version UID */
    private static final long serialVersionUID = 1L;
    
    /** The name of this Manager. */
    protected String name;
    
    /** The coordinate frame of this Manager. */
    protected CoordinateFrame frame;
    
    /** List of <code>GestureGroups</code> registered on this Manager. */
    protected Collection<GestureGroup> gestures;
    
    /** List of <code>GestureListeners</code> registered on this Manager. */
    protected Collection<GestureListener> listeners;
    
    /** If this Manager is currently recording a gesture or not. */
    protected transient boolean inGesture = false;
    
    
    
    
    
    /**
     * Creates a new instance of <code>GestureManager</code>.
     *
     * @param name The name of this Manager.
     * @param frame The coordinate frame of this Manager.
     */
    protected GestureManager(final String name, final CoordinateFrame frame) {
        this(name, frame, new ArrayList<GestureGroup>(), new ArrayList<GestureListener>());
    }
    
    /**
     * Creates a new instance of <code>GestureManager</code>.
     *
     * @param  name The name of this Manager.
     * @param  frame The coordinate frame of this Manager.
     * @param  gestures The backing list collection type for mouse gestures.
     * @param  listeners The backing list collection type for gesture listeners.
     * @throws java.lang.IllegalArgumentException If a <code>null</code> 
     *         argument is passed for either gestures or listeners.
     */
    protected GestureManager(final String name, final CoordinateFrame frame,
                             final Collection<GestureGroup> gestures, 
                             final Collection<GestureListener> listeners) 
        throws IllegalArgumentException
    {
        if (gestures == null) {
            throw new IllegalArgumentException("Backing gesture list was null!");
        }
        if (listeners == null) {
            throw new IllegalArgumentException("Backing listener list was null!");
        }
        
        
        this.name      = name;
        this.frame     = frame;
        this.gestures  = gestures;
        this.listeners = listeners;
    }
    
    
    
    
    
    /**
     * Sets the name of this Manager.  Any legal <code>String</code> may be used
     * as well as <code>null</code>.
     * 
     * @param name The new name.
     */
    public void setName(final String name) {
        this.name = name;
    }
    
    /**
     * Gets the name of this Manager, may be <code>null</code> if no name has
     * been set.
     * 
     * @return The name or <code>null</code>.
     */
    public String getName() {
        return name;
    }
    
    /**
     * Adds a new <code>GestureGroup</code> to this Manager.  All added 
     * <code>GestureGroups</code> get checked by the Manager when a gesture has 
     * been performed on the registered 'component'.
     * <p>
     * If <code>null</code> is passed no <code>GestureGroup</code> is added and 
     * no exception is thrown.
     * </p>
     *
     * @param group The <code>GestureGroup</code> to add.
     */
    public void addGestureGroup(GestureGroup group) {
        if (group != null) {
            gestures.add(group);
        }
    }
    
    /**
     * Removes a <code>GestureGroup</code> that has been added to this Manager.  
     * Once a <code>GestureGroup</code> is removed it is no longer checked by
     * this Manager when a gesture is performed on its registered 'component'.
     * <p>
     * If <code>null</code> is passed or the passed <code>GestureGroup</code>
     * was never added to this Manager then no action is taken and no exception
     * is thrown.
     * </p>
     * 
     * @param group The <code>GestureGroup</code> to remove.
     */
    public void removeGestureGroup(GestureGroup group) {
        if (group != null) {
            gestures.remove(group);
        }
    }
    
    /**
     * Returns all the <code>GestureGroups</code> added to this Manager.
     * <p>
     * If no <code>GestureGroups</code> have been added an empty array is
     * returned.
     * </p>
     * 
     * @return All the added <code>GestureGroups</code>.
     */
    public GestureGroup[] getGestureGroups() {
        return gestures.toArray(new GestureGroup[gestures.size()]);
    }
    
    /**
     * Adds a <code>GestureListener</code> to this Manager.
     * <p>
     * If <code>null</code> is passed then no action is taken and no
     * exception is thrown.
     * </p>
     * 
     * @param l The listener to add.
     */
    public void addGestureListener(GestureListener l) {
        if (l != null) {
            listeners.add(l);
        }
    }
    
    /**
     * Removes a <code>GestureListener</code> that has been added to this
     * Manager.
     * <p>
     * If <code>null</code> is passed or the passed listener was never added
     * to this Manager then no action is taken and no exception is thrown.
     * </p>
     * 
     * @param l The listener to remove.
     */
    public void removeGestureListener(GestureListener l) {
        if (l != null) {
            listeners.remove(l);
        }
    }
    
    /**
     * Returns all the <code>GestureListeners</code> added to this Manager.
     * <p>
     * If no listeners have been added an empty array is returned.
     * </p>
     * 
     * @return All the added gesture listeners.
     */
    public GestureListener[] getGesturesListeners() {
        return listeners.toArray(new GestureListener[listeners.size()]);
    }

    /**
     * Returns if this Manager is currently recording a gesture.
     * 
     * @return <code>true</code> if it is, <code>false</code> otherwise.
     */
    public boolean isInGesture() {
        return inGesture;
    }
    
    
    
    
    
    /**
     * Creates a <code>MouseMovement</code> that is independent of this
     * Managers coordinate frame.
     * <p>
     * Example:
     * </p><p>
     * If the coordinate frame of a window was {@link CoordinateFrame#Top_Left_Front}
     * then moving the mouse 'up' would result in a negative y movement.
     * However on a window with the coordinate frame of 
     * {@link CoordinateFrame#Bottom_Left_Front} moving the mouse 'up' would result
     * in a positive y movement.
     * </p><p>
     * Therefore using the movements gained from a window directly would
     * produce two different <code>MouseMovements</code> (the length would
     * be identical in both cases but the angle would not be).  This method
     * uses the Managers coordinate frame to compensate for this behaviour
     * so that the same <code>MouseMovements</code> will always be produced.
     * </p>
     * 
     * @param  start The start location.
     * @param  end The end location.
     * @throws java.lang.IllegalArgumentException If a <code>null</code>
     *         argument is passed.
     * @return The <code>MouseMovement</code>.
     */
    public MouseMovement createFrameIndependantMovement(Location start, Location end) 
        throws IllegalArgumentException
    {
        if (null == start) {
            throw new IllegalArgumentException("start was null!");
        }
        if (null == end) {
            throw new IllegalArgumentException("end was null!");
        }
        

        return createFrameIndependantMovement(start.getX(), start.getY(), 
                                              end.getX(),   end.getY());
    }
    
    /**
     * Creates a <code>MouseMovement</code> that is independent of this
     * Managers coordinate frame.
     * <p>
     * Example:
     * </p><p>
     * If the coordinate frame of a window was {@link CoordinateFrame#Top_Left_Front}
     * then moving the mouse 'up' would result in a negative y movement.
     * However on a window with the coordinate frame of 
     * {@link CoordinateFrame#Bottom_Left_Front} moving the mouse 'up' would result
     * in a positive y movement.
     * </p><p>
     * Therefore using the movements gained from a window directly would
     * produce two different <code>MouseMovements</code> (the length would
     * be identical in both cases but the angle would not be).  This method
     * uses the Managers coordinate frame to compensate for this behaviour
     * so that the same <code>MouseMovements</code> will always be produced.
     * </p>
     *
     * @param  x1 The start x coordinate.
     * @param  y1 The start y coordinate.
     * @param  x2 The end x coordinate.
     * @param  y2 The end y coordinate.
     * @return The <code>MouseMovement</code>.
     */
    public MouseMovement createFrameIndependantMovement(float x1, float y1, 
                                                        float x2, float y2) 
    {
        float x = x2 - x1;
        float y = y2 - y1;
        
        return new MouseMovement(
                    (float) Math.hypot(x, y), frame.correctFrameAngle(x, y));
    }
    
    /**
     * Starts a new gesture and notifies any listeners.
     * <p>
     * This method does nothing if the Manager is already recording a
     * gesture (although it will still throw a <code>NullPointerException</code>
     * if <code>null</code> is passed).
     * </p>
     *
     * @param  location The location the gesture was started.
     * @throws java.lang.IllegalArgumentException If a <code>null</code> argument is
     *         passed.
     */
    public void startGesture(Location location) 
        throws IllegalArgumentException
    {
        if (null == location) {
            throw new IllegalArgumentException("location was null!");
        }
        
        
        if (!inGesture) {
            
            inGesture = true;
            
            long when = System.currentTimeMillis();
            
            for (GestureListener l : listeners) {
                l.gestureStarted(when, location, this);
            }
        }
    }
    
    /**
     * Ends the current gesture and notifies any listeners.
     * <p>
     * This method does nothing if the Manager is <strong>not</strong> already 
     * recording a gesture (although it will still throw a 
     * <code>NullPointerException</code> if <code>null</code> is passed).
     * </p>
     *
     * @param  location The location the gesture was ended.
     * @throws java.lang.IllegalArgumentException If a <code>null</code> 
     *         argument is passed.
     */
    public void endGesture(Location location) 
        throws IllegalArgumentException
    {
        if (null == location) {
            throw new IllegalArgumentException("location was null!");
        }
        
        
        if (inGesture) {
            inGesture = false;
            
            long when = System.currentTimeMillis();

            for (GestureListener l : listeners) {
                l.gestureFinished(when, location, this);
            }
        }
    }

    /**
     * Checks to see if a gesture has been performed and notifies any
     * listeners if one was.
     * <p>
     * If <code>null</code> or an empty list is passed then no action is taken
     * and no exception is thrown.
     * </p>
     *
     * @param mouseMovements The <code>MouseMovements</code> to check against.
     */
    public void checkForPerformedGesture(Collection<MouseMovement> mouseMovements) {  
        if (null == mouseMovements || mouseMovements.isEmpty()) {
            return;
        }

        
        checkForPerformedGesture(
            mouseMovements.toArray(new MouseMovement[mouseMovements.size()]));
    }
    
    /**
     * Checks to see if a gesture has been performed and notifies any
     * listeners if one was.
     * <p>
     * If <code>null</code> or an empty list is passed then no action is taken
     * and no exception is thrown.
     * </p>
     *
     * @param mouseMovements The <code>MouseMovements</code> to check against.
     */
    public void checkForPerformedGesture(MouseMovement ... mouseMovements) {  
        if (null == mouseMovements || 0 == mouseMovements.length) {
            return;
        }
        
        
        for (GestureGroup group : gestures) {
            group.checkAndNotify(mouseMovements);
        }
    }
    
    
    
    
    
    /**
     * Registers the given 'component'.  The registered component is the
     * object that the Manager will check to see if mouse gestures were
     * performed 'on it'.
     * <p>
     * Subclasses will need to actually implement this behaviour.
     * </p>
     * 
     * @param me The 'component' to register with.
     * @see   #unregisterComponent
     */
    public abstract void registerComponent(C me);
    
    /**
     * This un-registers the registered 'component'.  If a 'component' was
     * never registered no action should be taken.
     * <p>
     * Subclasses will need to actually implement this behaviour.
     * </p>
     *
     * @see #registerComponent
     */
    public abstract void unregisterComponent();
    
    /**
     * Gets the registered 'component'.
     * <p>
     * Subclasses will need to actually implement this behaviour.
     * </p>
     * 
     * @return The registered 'component' or <code>null</code>.
     */
    public abstract C getRegisteredComponent();
    
    /**
     * Gets the mouse modifiers needed for a gesture to be started.
     * <p>
     * Subclasses will need to actually implement this behaviour and define
     * what the modifier values are.
     * </p>
     * 
     * @return The mouse modifiers.
     */
    public abstract int getMouseModifiers();
    
    /**
     * Adds a mouse modifier.
     * <p>
     * Subclasses will need to actually implement this behaviour and define
     * what the modifier values are.
     * </p>
     * 
     * @param mod The modifier to add.
     */
    public abstract void addMouseModifier(int mod);
    
    /**
     * Removes a mouse modifier.  If the given modifier has not been added
     * then no action should be taken.
     * <p>
     * Subclasses will need to actually implement this behaviour and define
     * what the modifier values are.
     * </p>
     * 
     * @param mod The modifier to remove.
     */
    public abstract void removeMouseModifier(int mod);
    
    /**
     * Gets the key modifiers needed for a gesture to be started.
     * <p>
     * Subclasses will need to actually implement this behaviour and define
     * what the modifier values are.
     * </p>
     * 
     * @return The keyboard modifiers.
     */
    public abstract int getKeyboardModifiers();
    
    /**
     * Adds a keyboard modifier.
     * <p>
     * Subclasses will need to actually implement this behaviour and define
     * what the modifier values are.
     * </p>
     * 
     * @param mod The modifier to add.
     */
    public abstract void addKeyboardModifier(int mod);
    
    /**
     * Removes a keyboard modifier.  If the given modifier has not been added
     * then no action should be taken.
     * <p>
     * Subclasses will need to actually implement this behaviour and define
     * what the modifier values are.
     * </p>
     * 
     * @param mod The modifier to remove.
     */
    public abstract void removeKeyboardModifier(int mod);

    
    
    
    
    /**
     * Returns a <code>String</code> representation of this 
     * <code>GestureManager</code>.
     * 
     * @return The <code>String</code> representation.
     */
    @Override
    public String toString() {
        StringBuilder buf = new StringBuilder(50);
        
        buf.append("name=").append(name);
        buf.append(",frame=").append(frame);
        buf.append(",gestures").append(gestures);
        buf.append(",listeners=").append(listeners);
        
        return buf.toString();
    }
    
    /**
     * Indicates whether some other object is "equal to" this one.
     * 
     * @param  obj The reference object with which to compare.
     * @return <code>true</code> if this object is the same as the obj
     *         argument, <code>false</code> otherwise.
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        
        if (null == obj || obj.getClass() != getClass()) {
            return false;
        }
            
        GestureManager<?> gm = (GestureManager<?>) obj;

        return (null == gm.name 
                    ? null == name
                    : gm.name.equals(name)) &&
               gm.frame.equals(frame)       &&
               gm.gestures.equals(gestures) &&
               gm.listeners.equals(listeners);
    }
    
    /**
     * Returns a hash code value for the object.
     * 
     * @return A hash code value for this object.
     */
    @Override
    public int hashCode() {
        return (null == name
                    ? 37
                    : name.hashCode()) * 13 +
               frame.hashCode()        * 13 +
               gestures.hashCode()     * 13 +
               listeners.hashCode()    * 13;
    }
    
}
